package org.jflame.devAide.plugin.codeFormatter;

import java.io.IOException;
import java.io.StringReader;
import java.net.URL;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.ServiceLoader;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.controlsfx.control.textfield.CustomTextField;
import org.controlsfx.glyphfont.FontAwesome;
import org.jflame.commons.util.CollectionHelper;
import org.jflame.commons.util.StringHelper;
import org.jflame.devAide.util.ResourceUtils;
import org.jflame.devAide.util.UIUtils;
import org.jflame.fx.util.TreeViewUtils;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.JSONValidator;
import com.alibaba.fastjson.JSONValidator.Type;

import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TextArea;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.util.StringConverter;

/**
 * 格式化工具
 * 
 * @author yucan.zhang
 */
public class FormatToolController implements Initializable {

    @FXML
    private Button btnJsonFmt;
    @FXML
    private Button btnSearch;
    @FXML
    private TextArea srcText;
    @FXML
    private TextArea targetText;
    @FXML
    private AnchorPane targetBox;
    @FXML
    private ComboBox<CodeFormatter> cbxCodeFormat;
    @FXML
    private CustomTextField txtFind;
    @FXML
    private Button btnFind;
    @FXML
    private ChoiceBox<String> choView;
    @FXML
    private VBox resultPane;

    private TreeView<String> treeView = null;
    private TreeItem<String> rootNode = null;

    private List<CodeFormatter> formatters = new ArrayList<>(5);

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        initCodeFormatter();
        bindFormatterToCombobox();
        bindChoView();
        // createMenuItem();

        txtFind.setLeft(UIUtils.createFontGlyph(FontAwesome.Glyph.SEARCH, 14)
                .color(Color.LIGHTGRAY));
    }

    private void initCodeFormatter() {
        ServiceLoader<CodeFormatter> sloader = ServiceLoader.load(CodeFormatter.class);
        Iterator<CodeFormatter> it = sloader.iterator();
        while (it.hasNext()) {
            formatters.add(it.next());
        }
        // 显式声明，不在module-info定义provider
        ComplexFormatter autoFormatter = new ComplexFormatter(new ArrayList<>(formatters));
        formatters.add(0, autoFormatter);
    }

    private void bindFormatterToCombobox() {
        cbxCodeFormat.setConverter(new CodeFormatterConverter());
        if (CollectionHelper.isNotEmpty(formatters)) {
            ObservableList<CodeFormatter> cbxData = FXCollections.observableArrayList(formatters);
            cbxCodeFormat.setItems(cbxData);
            cbxCodeFormat.getSelectionModel()
                    .select(0);
        }
    }

    private String choOptTree = ResourceUtils.getString("plugin.formatter.tree");
    private String choOptText = ResourceUtils.getString("plugin.formatter.txt");
    private String curCodeFormat;
    private String[] treeableFormats = { "JSON","XML" };

    private void bindChoView() {
        choView.setItems(FXCollections.observableArrayList(choOptText, choOptTree));
        choView.getSelectionModel()
                .select(0);
        choView.getSelectionModel()
                .selectedItemProperty()
                .addListener(new ChangeListener<String>() {

                    @Override
                    public void changed(ObservableValue<? extends String> observable, String oldValue,
                            String newValue) {
                        if (choOptTree.equals(newValue)) {
                            try {
                                buildTreeView(targetText.getText());
                            } catch (Exception e) {
                                UIUtils.showException("构建树形视图异常", e);
                            }
                        } else {
                            toggleTreeView(false);
                        }
                    }
                });
    }

    /**
     * 格式化事件处理
     * 
     * @param event
     */
    @FXML
    private void formattHandle(ActionEvent event) {
        String codeText = srcText.getText();
        String formattedStr = null;
        targetText.setText(null);
        if (StringHelper.isNotEmpty(codeText)) {
            final String srcStr = codeText.strip();
            if (!cbxCodeFormat.getSelectionModel()
                    .isEmpty()) {
                CodeFormatter formatter = null;
                try {
                    formatter = cbxCodeFormat.getSelectionModel()
                            .getSelectedItem();
                    formattedStr = formatter.convert(srcStr);
                } catch (Exception e) {
                    UIUtils.showException("%plugin.formatter.unableformat", e);
                    return;
                }
                if (formatter instanceof ComplexFormatter) {
                    curCodeFormat = ((ComplexFormatter) formatter).getCurrentFormatter()
                            .getCodeFormat();
                } else {
                    curCodeFormat = formatter.getCodeFormat();
                }

                if (ArrayUtils.contains(treeableFormats, curCodeFormat)) {
                    choView.setDisable(false);
                    if (choOptTree.equals(choView.getValue())) {
                        try {
                            buildTreeView(formattedStr);
                        } catch (Exception e) {
                            UIUtils.showException("构建树形视图异常", e);
                        }
                        targetText.setText(formattedStr);
                        return;
                    }
                } else {
                    choView.setValue(choOptText);
                    choView.setDisable(true);
                }
                toggleTreeView(false);
                targetText.setText(formattedStr);

            } else {
                UIUtils.warn("%plugin.formatter.unrecognized");
            }

        }
    }

    private void buildTreeView(String formattedStr) throws Exception {
        if (StringUtils.isBlank(formattedStr)) {
            return;
        }
        if (curCodeFormat.equals(treeableFormats[0])) {
            createJsonTreeView(formattedStr);
        } else if (curCodeFormat.equals(treeableFormats[1])) {
            createXmlTreeView(formattedStr);
            txtFind.setPromptText("xpath");
            txtFind.setUserData("xpath");
        }

        treeView.requestFocus();
    }

    String lastKeyword;

    /**
     * 查找事件处理
     * 
     * @param event
     */
    @FXML
    private void handleFind(ActionEvent event) {
        String keyword = txtFind.getText();
        String text = targetText.getText();
        if (StringUtils.isBlank(keyword) || StringUtils.isBlank(text)) {
            return;
        }

        keyword = keyword.trim();
        if ("xpath".equals(txtFind.getUserData())) {
            TreeViewUtils.loop(treeView, item -> {
                if (item instanceof StyledTreeItem) {
                    ((StyledTreeItem<String>) item).clearStyleClass();
                }
            });
            String style = "highlight";
            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
            try {
                DocumentBuilder db = dbf.newDocumentBuilder();
                InputSource is = new InputSource(new StringReader(text));
                Document xmlDoc = db.parse(is);

                XPathFactory xPathFactory = XPathFactory.newInstance();
                XPath xpath = xPathFactory.newXPath();
                try {
                    NodeList nodeList = (NodeList) xpath.evaluate(keyword, xmlDoc, XPathConstants.NODESET);

                    for (int i = 0; i < nodeList.getLength(); i++) {
                        String nodeText = createTreeNodeText(nodeList.item(i));
                        if (nodeText != null) {
                            TreeViewUtils.loop(treeView, item -> {
                                if (item instanceof StyledTreeItem) {
                                    ((StyledTreeItem<String>) item).setStyleClass(style);
                                }
                            });
                        }
                    }

                } catch (XPathExpressionException e) {
                    UIUtils.info("%plugin.formatter.invalidxpath");
                    return;
                } catch (IllegalArgumentException e) {
                    UIUtils.info("%plugin.formatter.notsupportxpath=");
                    return;
                }
            } catch (Exception e) {
                UIUtils.showException("xpath查找异常", e);
            }
        }
        /*String srcStr = srcText.getText();
        if (StringHelper.isNotEmpty(srcStr)) {
            int startIndex = keyword.equals(lastKeyword) && srcText.getUserData() != null
                    ? (Integer) srcText.getUserData()
                    : 0;
            startIndex = srcStr.indexOf(keyword, startIndex);
            if (startIndex >= 0) {
                srcText.selectRange(startIndex, startIndex + keyword.length());
                btnSearch.setText("下一个");
                srcText.setUserData(startIndex);
                lastKeyword = keyword;
            } else {
                UIUtils.alert("提示", "查找无结果");
                btnSearch.setText("查找");
                lastKeyword = null;
                srcText.setUserData(null);
            }
        }*/
    }

    /**
     * 创建json treeview
     * 
     * @param jsonText
     */
    private void createJsonTreeView(String jsonText) {
        JSONValidator validator = JSONValidator.from(jsonText);
        if (!validator.validate()) {
            return;
        }
        toggleTreeView(true);
        TreeViewUtils.clear(treeView);

        if (validator.getType() == Type.Array) {
            JSONArray root = JSON.parseArray(jsonText);
            createJsonTreeNode(root, rootNode);
        } else {
            JSONObject root = JSON.parseObject(jsonText);
            createJsonTreeNode(root, rootNode);
        }

        treeView.getRoot()
                .setExpanded(true);
        treeView.refresh();
    }

    private void createJsonTreeNode(JSON jnode, TreeItem<String> parent) {
        String nodeName;
        TreeItem<String> curItem;
        if (jnode instanceof JSONArray) {
            JSONArray root = (JSONArray) jnode;
            int size = root.size();
            for (int i = 0; i < size; i++) {
                Object node = root.get(i);
                nodeName = "[" + i + "]:";
                if (node instanceof JSONObject) {
                    nodeName = nodeName + "[Object]";
                    curItem = new TreeItem<String>(nodeName);
                    parent.getChildren()
                            .add(curItem);
                    // System.out.println(nodeName);
                    if (!((JSONObject) node).isEmpty()) {
                        createJsonTreeNode((JSONObject) node, curItem);
                    }
                } else if (node instanceof JSONArray) {
                    nodeName = nodeName + "[Array]";
                    curItem = new TreeItem<String>(nodeName);
                    parent.getChildren()
                            .add(curItem);
                    // System.out.println(nodeName);
                    if (!((JSONArray) node).isEmpty()) {
                        createJsonTreeNode((JSONArray) node, curItem);
                    }
                } else {
                    nodeName = nodeName + node;
                    curItem = new TreeItem<String>(nodeName);
                    parent.getChildren()
                            .add(curItem);
                }
            }
        } else if (jnode instanceof JSONObject) {
            JSONObject root = (JSONObject) jnode;
            for (Map.Entry<String,Object> kv : root.entrySet()) {
                nodeName = kv.getKey() + ":";
                if (kv.getValue() instanceof JSONObject) {
                    nodeName = nodeName + "[Object]";
                    curItem = new TreeItem<String>(nodeName);
                    parent.getChildren()
                            .add(curItem);
                    // System.out.println(nodeName);
                    if (!((JSONObject) kv.getValue()).isEmpty()) {
                        createJsonTreeNode((JSONObject) kv.getValue(), curItem);
                    }
                } else if (kv.getValue() instanceof JSONArray) {
                    nodeName = nodeName + "[Array]";
                    curItem = new TreeItem<String>(nodeName);
                    parent.getChildren()
                            .add(curItem);
                    // System.out.println(nodeName);
                    if (!((JSONArray) kv.getValue()).isEmpty()) {
                        createJsonTreeNode((JSONArray) kv.getValue(), curItem);
                    }
                } else {
                    nodeName = nodeName + kv.getValue();
                    curItem = new TreeItem<String>(nodeName);
                    parent.getChildren()
                            .add(curItem);
                    // System.out.println(nodeName);
                }
            }
        }
    }

    private void createXmlTreeView(String xmlText) throws ParserConfigurationException, SAXException, IOException {

        toggleTreeView(true);
        TreeViewUtils.clear(treeView);

        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        DocumentBuilder db = dbf.newDocumentBuilder();
        InputSource is = new InputSource(new StringReader(xmlText.replaceAll(">(\\s*|\n|\t|\r)<", "><")));
        Document xmlDoc = db.parse(is);
        if (xmlDoc.hasChildNodes()) {
            treeView.setShowRoot(false);
            createXmlTreeNode(xmlDoc.getFirstChild(), rootNode);
            TreeViewUtils.expandAll(rootNode);
        }

    }

    private void createXmlTreeNode(Node node, TreeItem<String> parent) {
        String nodeName = createTreeNodeText(node);
        if (nodeName != null) {
            // System.out.println("node:" + node.getNodeName() + " t:" + node.getNodeType() + " v:" +
            // node.getNodeValue());
            StyledTreeItem<String> curItem = new StyledTreeItem<String>(nodeName);
            parent.getChildren()
                    .add(curItem);
            if (node.getNodeType() == Node.ELEMENT_NODE && node.hasChildNodes()) {
                int childSize = node.getChildNodes()
                        .getLength();
                for (int i = 0; i < childSize; i++) {
                    createXmlTreeNode(node.getChildNodes()
                            .item(i), curItem);
                }
            }
        }
    }

    private String createTreeNodeText(Node node) {
        String nodeName = null;
        if (node.getNodeType() == Node.ELEMENT_NODE) {
            if (node.hasAttributes()) {
                nodeName = "<" + node.getNodeName();
                NamedNodeMap attriMap = node.getAttributes();
                for (int n = 0; n < attriMap.getLength(); n++) {
                    nodeName = nodeName + " " + attriMap.item(n)
                            .getNodeName() + "=\""
                            + attriMap.item(n)
                                    .getNodeValue()
                            + "\"";
                }
                nodeName = nodeName + ">";
            } else {
                nodeName = "<" + node.getNodeName() + ">";
            }
        } else if (node.getNodeType() == Node.TEXT_NODE) {
            if (StringUtils.isNotBlank(node.getNodeValue())) {
                nodeName = node.getNodeValue();
            }
        }
        return nodeName;
    }

    /* private PopOver createPopOver() {
        PopOver popOver = new PopOver();
        popOver.setDetached(true);
        popOver.setArrowIndent(5);
        popOver.setArrowSize(20);
        popOver.setCornerRadius(3);
        popOver.setArrowLocation(ArrowLocation.LEFT_CENTER);
        popOver.setAnimated(true);
        popOver.setCloseButtonEnabled(false);
        return popOver;
    }*/
    /*    private void createMenuItem() {
        ContextMenu textAreaMenu = new ContextMenu();
    
        MenuItem mitemCopy = new MenuItem("复制");
        mitemCopy.setOnAction(e -> {
            srcText.copy();
        });
        MenuItem mitemPaste = new MenuItem("粘贴");
        mitemPaste.setOnAction(e -> {
            srcText.paste();
        });
        textAreaMenu.setOnShowing(e -> {
            final Clipboard clipboard = Clipboard.getSystemClipboard();
            System.out.println("剪切版内容:" + clipboard.getString());
            if (!clipboard.hasString()) {
                mitemPaste.setDisable(true);
            }
        });
        textAreaMenu.getItems()
                .addAll(mitemCopy, mitemPaste);
        srcText.setContextMenu(textAreaMenu);
    }*/

    private void toggleTreeView(boolean isShow) {
        if (isShow) {
            if (treeView == null) {
                initTreeView();
            }
            resultPane.getChildren()
                    .clear();
            resultPane.getChildren()
                    .add(treeView);
        } else {
            resultPane.getChildren()
                    .clear();
            resultPane.getChildren()
                    .add(targetText);
            if (treeView != null) {
                TreeViewUtils.clear(treeView);
            }
        }
    }

    private void initTreeView() {
        rootNode = new TreeItem<String>("ROOT");
        treeView = new TreeView<>();
        treeView.setRoot(rootNode);
        treeView.setPrefSize(targetText.getPrefWidth(), targetText.getPrefHeight());

        ContextMenu treeContextMenu = new ContextMenu();
        MenuItem expandMenu = new MenuItem(ResourceUtils.getString("pub.expand"));
        expandMenu.setOnAction(e -> {
            TreeViewUtils.expandAll(treeView.getSelectionModel()
                    .getSelectedItem());
        });
        MenuItem collapseMenu = new MenuItem(ResourceUtils.getString("pub.collaspe"));
        collapseMenu.setOnAction(e -> {
            TreeViewUtils.collapseAll(treeView.getSelectionModel()
                    .getSelectedItem());
        });
        treeContextMenu.getItems()
                .addAll(expandMenu, collapseMenu);
        treeView.setContextMenu(treeContextMenu);

        VBox.setVgrow(treeView, Priority.ALWAYS);
    }

    private final class CodeFormatterConverter extends StringConverter<CodeFormatter> {

        @Override
        public String toString(CodeFormatter object) {
            return object.getCodeFormat();
        }

        @Override
        public CodeFormatter fromString(String string) {
            Optional<CodeFormatter> opt = formatters.stream()
                    .filter(f -> f.getCodeFormat()
                            .equals(string))
                    .findFirst();
            return opt.get();
        }
    }

    class StyledTreeItem<T> extends TreeItem<T> {

        private String styleClass;

        public StyledTreeItem() {
            this(null);
        }

        public StyledTreeItem(final T value) {
            this(value, (javafx.scene.Node) null);
        }

        public StyledTreeItem(final T value, final javafx.scene.Node graphic) {
            super(value, graphic);
        }

        public String getStyleClass() {
            return styleClass;
        }

        public void setStyleClass(String styleClass) {
            this.styleClass = styleClass;
        }

        public void clearStyleClass() {
            if (styleClass != null) {
                styleClass = null;
            }
        }

    }

    class StyledTreeCell<T> extends TreeCell<T> {

        private String styleClass = null;

        @Override
        protected void updateItem(T item, boolean empty) {
            super.updateItem(item, empty);
            TreeItem<T> titem = getTreeItem();
            if (titem instanceof StyledTreeItem) {
                String _styleClass = ((StyledTreeItem<T>) titem).getStyleClass();
                if (_styleClass != null) {
                    styleClass = _styleClass;
                    getStyleClass().add(styleClass);
                } else {
                    if (styleClass != null) {
                        getStyleClass().remove(styleClass);
                    }
                }
            }
        }
    }
}
